Ninereeds Broadcast Lib

By Steve Horne, 24 July 1999

This library supports communication of commands between machines, including broadcast commands that are sent to several machines at once. Unfortunately, machines must be modified to support this library. However, these changes are relatively simple.


Minimal Broadcast Lib Support

  1. Configure your machines project to link the file nblib.lib.

  2. Include the broadcast header file into the source, as follows...
    #include "../Ninereeds Broadcast Lib/Ninereeds Broadcast Lib.h"
    
  3. Update your 'mi' class definition to inherit from the broadcast listening class as well as the standard machine interface class, as follows...
    class mi : public CMachineInterface, public c_Broadcast_Listener_CB
    {
      ...
    };
    
  4. Add the following virtual function declarations to your mi class...
    virtual void Transpose_Notes  (int p_Semitones);
    
    virtual void All_Notes_Off    (void);
    virtual void All_Notes_Ignore (void);
    
    virtual void Set_Control      (int p_ID, int p_Value);
    
    virtual void Tick2 (void);
    virtual void Tick2 (int p_Channel, c_Broadcast_Params *p_Command);
    virtual bool Work2 (float *psamples, int numsamples, int const mode, int offset);
    
  5. In your mi::Init function, add the following code to start listening...
    Set_Master_Info (pMasterInfo);
    
    NR_Start_Listening (0, this);
    
  6. In your mi::~mi function, add the following code to stop listening...
    NR_Stop_Listening (0, this);
    
  7. Replace your mi::Tick and mi::Work functions with code similar to the following...
    void mi::Tick()
    {
      Handle_Tick ();
    }
    
    void mi::Tick2 (void)
    {
      //  Code from the original Tick function
    }
    
    void mi::Tick2 (int p_Channel, c_Broadcast_Params *p_Command)
    {
      //  Code to handle received broadcast commands
    }
    
    bool mi::Work (float *psamples, int numsamples, int const mode)
    {
      return Handle_Work (psamples, numsamples, mode);
    }
    
    bool mi::Work2(float *psamples, int numsamples, int const mode, int offset)
    {
      //  Code from original Work function
    }
    
    Note that there may be slight deviations from this - for example, to receive an AuxBus signal it is better to fetch the data in mi::Work, and update a pointer to the relevant part in mi::Work2.

  8. Add the callbacks to implement some standard commands - use the following examples as a model, but adjust according to the properties of your machine...
    void mi::Transpose_Notes  (int p_Semitones)
    {
      int l_Note;
    
      for (int i = 0; i < MAX_TRACKS; i++)
      {
        if (   (tval [i].noteinit != NOTE_NO )
            && (tval [i].noteinit != NOTE_OFF))
        {
          l_Note  = ((tval [i].noteinit >> 4) * 12) + (tval [i].noteinit & 0x0F);
          l_Note += p_Semitones;
          
          if ((l_Note >= NOTE_MIN) && (l_Note <= NOTE_MAX))
          {
            tval [i].noteinit = ((l_Note / 12) << 4) + (l_Note % 12);
          }
          else
          {
            tval [i].noteinit = NOTE_OFF;
          }
        }
    
        if (   (tval [i].note != NOTE_NO )
            && (tval [i].note != NOTE_OFF))
        {
          l_Note  = ((tval [i].note >> 4) * 12) + (tval [i].note & 0x0F);
          l_Note += p_Semitones;
          
          if ((l_Note >= NOTE_MIN) && (l_Note <= NOTE_MAX))
          {
            tval [i].note = ((l_Note / 12) << 4) + (l_Note % 12);
          }
          else
          {
            tval [i].noteinit = NOTE_OFF;
            tval [i].note     = NOTE_NO;
          }
        }
      }
    }
    
    void mi::All_Notes_Off    (void)
    {
      for (int i = 0; i < MAX_TRACKS; i++)
      {
        tval [i].noteinit = NOTE_OFF;
      }
    }
    
    void mi::All_Notes_Ignore (void)
    {
      for (int i = 0; i < MAX_TRACKS; i++)
      {
        if (   (tval [i].noteinit != NOTE_NO )
            && (tval [i].noteinit != NOTE_OFF))
        {
          tval [i].noteinit = NOTE_NO;
        }
    
        tval [i].note = NOTE_NO;
      }
    }
    
    void mi::Set_Control (int p_ID, int p_Value)
    {
      switch (p_ID)
      {
        case 0 :  //  May make a convention that this is always volume?
        {         //  On the other hand, volume is better handled by fade controls
          for (int i = 0; i < MAX_TRACKS; i++)
          {
            tval [i].volume = p_Value >> 8;
          }
    
          break;
        }
      }
    }
    

    Important - if any of the callbacks above are not to be handled, they must still by defined. In this case, provide the functions as above but do not include the code.

    The reason that there are no default functions for this is to ensure that exclusions are a decision rather than through forgetting.


Selecting the Channel and Controls

The above setup will work, but it has major limitations. The most important is that the machine always listens to channel 0. A machine should support any channel from 0 to 7, should be able to stop listening, and should be able to save this state information in the machine specific part of a Buzz file.

The easiest way to achieve this is to use the dialog that is built into the broadcast library DLL. This also supports configuration of a machines controls. To do this...

  1. Add an option to your machines command menu. This is the last field in the machine info...
    CMachineInfo const MacInfo =
    {
      MT_GENERATOR, MI_VERSION, 0,     // type, version, flags
      1, MAX_TRACKS,                   // min, max tracks
      0, 3, pParameters,               // num globalpars, num trackpars, *pars
      0,    NULL,                      // num attribs, *attribs
      "Ninereeds NRS04", "NRS04", "Steve Horne",  // name, short name, author
      "Edit Ninereeds NRS04"           // command menu
    };
    
  2. Add the command and save functions to your machines mi class...
    virtual void Command (int const i);
    virtual void Save    (CMachineDataOutput * const po);
    
  3. Add the fields to store the listen enable flag and channel number to your machines mi class...
    bool  f_Listen_Enabled;
    int   f_Listen_Channel;
    
  4. Define the available controls, and implement the mi::Command function...
    #define NUM_CONTROL_CONFIGS 6
    
    c_Control_Config g_Control_Configs [NUM_CONTROL_CONFIGS] =
    {
      {0, "Volume"             },
      {1, "Attack"             },
      {2, "Decay"              },
      {3, "Bend Rate"          },
      {4, "Fractal Effect Low" },
      {5, "Fractal Effect High"}
    };
    
    void mi::Command(int const i)
    {
      switch (i)
      {
        case 0 :  //  Edit Ninereeds Softy
        {
          NR_Dialog_Enable_Channel (this, &f_Listen_Enable, &f_Listen_Channel,
                                    NUM_CONTROL_CONFIGS, g_Control_Configs    );
        }
    
        break;
      }
    }
    
  5. Add initialisation to mi::mi to ensure that listening is turned off.

  6. Update mi::Init to read the listening enable flag, channel and control configuration from the file, and to call the start listening function if appropriate.

  7. Implement mi::Save to write the listening enable flag and channel to the file, and to save the control configuration.

  8. Modify mi::~mi to call the end listening function only if listening is enabled.

Example code is not given here for the initialisation and file read/write stuff as this is fairly simple, and because it is dependant on how your machine saves existing data. However, I do recommend a format where an initial 'block identifier' word indicates what type of data follows, and what version of that data is stored. This allows a lot of freedom in upgrading, to add new features (new block types) and to upgrade existing features (new block versions), without losing backwards compatability.

Do remember when implementing mi::Init to check if pi is NULL before trying to read data back - pi will be NULL for any buzz files that did not have machine specific data recorded.

Finally, the functions to load and save the control configurations are...

Read_Controls  (pi);
Write_Controls (po);


Supporting Additional Commands

The level of support described above is easily implemented - less the 15 mins. for my Ninereeds NRS02 machine. It is also useful, if only for the transpose facility. However, it is really only the beginning. The real benefits come from the additional commands and controls that you make available.

Controls are very easy to set up - just add an item to your control list, and add the implementation to the Set_Control method. It is possible to use them for things you don't have parameters for, but really its just a way of controlling machine parameters externally with the added bonus of automatic control slides.

In particular, controls have no more precision of time or value than is available to normal parameters.

Machine specific commands require significantly more work, but are not subject to these limitations.